﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Text.RegularExpressions;

namespace Utils.JobTask
{
    public class Cron
    {

        public static readonly int MaxYear;

        private static readonly char[] splitSeparators;

        private static readonly char[] commaSeparator;

        private static readonly Regex regex;

        private static readonly Dictionary<string, int> monthMap;

        private static readonly Dictionary<string, int> dayMap;

        static Cron()
        {
            monthMap = new Dictionary<string, int>(20);
            dayMap = new Dictionary<string, int>(60);
            MaxYear = DateTime.Now.Year + 100;
            splitSeparators = new char[4]
            {
                ' ',
                '\t',
                '\r',
                '\n'
            };
            commaSeparator = new char[1]
            {
                ','
            };
            regex = new Regex("^L-[0-9]*[W]?", RegexOptions.Compiled);
            monthMap.Add("JAN", 0);
            monthMap.Add("FEB", 1);
            monthMap.Add("MAR", 2);
            monthMap.Add("APR", 3);
            monthMap.Add("MAY", 4);
            monthMap.Add("JUN", 5);
            monthMap.Add("JUL", 6);
            monthMap.Add("AUG", 7);
            monthMap.Add("SEP", 8);
            monthMap.Add("OCT", 9);
            monthMap.Add("NOV", 10);
            monthMap.Add("DEC", 11);
            dayMap.Add("SUN", 1);
            dayMap.Add("MON", 2);
            dayMap.Add("TUE", 3);
            dayMap.Add("WED", 4);
            dayMap.Add("THU", 5);
            dayMap.Add("FRI", 6);
            dayMap.Add("SAT", 7);
        }

        private readonly string cronExpression;
        private readonly SortedSet<int> seconds;
        private readonly SortedSet<int> minutes;
        private readonly SortedSet<int> hours;
        private readonly SortedSet<int> daysOfMonth;



        private readonly SortedSet<int> months;
        private readonly SortedSet<int> daysOfWeek;
        private readonly SortedSet<int> years;


        private bool lastdayOfWeek;

        private int everyNthWeek;

        private int nthdayOfWeek;

        private bool lastdayOfMonth;

        private int lastdayOffset;

        private bool nearestWeekday;
        private bool calendardayOfWeek;
        private bool calendardayOfMonth;

        public Cron(string cronExpression)
        {
            this.cronExpression = cronExpression;
            seconds = new SortedSet<int>();
            minutes = new SortedSet<int>();
            hours = new SortedSet<int>();
            daysOfMonth = new SortedSet<int>();
            months = new SortedSet<int>();
            daysOfWeek = new SortedSet<int>();
            years = new SortedSet<int>();
            this.BuildExpression(cronExpression);
        }

        public bool IsSatisfiedByNow()
        {
            return IsSatisfiedBy(DateTimeOffset.Now);
        }

        public bool IsSatisfiedBy(DateTimeOffset dateUtc)
        {


            DateTimeOffset other = new DateTimeOffset(dateUtc.Year, dateUtc.Month, dateUtc.Day, dateUtc.Hour, dateUtc.Minute, dateUtc.Second, dateUtc.Offset);
            if (!seconds.Contains(other.Second))
            {
                return false;
            }
            if (!minutes.Contains(other.Minute))
            {
                return false;
            }
            if (!hours.Contains(other.Hour))
            {
                return false;
            }
            if (!daysOfMonth.Contains(other.Day) && daysOfMonth.LastOrDefault() < 90)
            {
                return false;
            }
            if (!months.Contains(other.Month))
            {
                return false;
            }
            if (!daysOfWeek.Contains((int)other.DayOfWeek + 1) && daysOfWeek.LastOrDefault() < 90)
            {
                return false;
            }
            if (!years.Contains(other.Year))
            {
                return false;
            }
            if (lastdayOfWeek || nearestWeekday || lastdayOfMonth || calendardayOfWeek || calendardayOfMonth)
            {

            }

            return true;
        }

        private void BuildExpression(string expression)
        {
            try
            {

                int num = 0;
                string[] array = expression.Split(splitSeparators, StringSplitOptions.RemoveEmptyEntries);
                string[] array2 = array;
                foreach (string text in array2)
                {
                    string text2 = text.Trim();
                    if (text2.Length != 0)
                    {
                        if (num > 6)
                        {
                            break;
                        }

                        if (num == 3 && text2.IndexOf('L') != -1 && text2.Length > 1 && text2.IndexOf(",", StringComparison.Ordinal) >= 0)
                        {
                            throw new FormatException("Support for specifying 'L' and 'LW' with other days of the month is not implemented");
                        }

                        if (num == 5 && text2.IndexOf('L') != -1 && text2.Length > 1 && text2.IndexOf(",", StringComparison.Ordinal) >= 0)
                        {
                            throw new FormatException("Support for specifying 'L' with other days of the week is not implemented");
                        }

                        if (num == 5 && text2.IndexOf('#') != -1 && text2.IndexOf('#', text2.IndexOf('#') + 1) != -1)
                        {
                            throw new FormatException("Support for specifying multiple \"nth\" days is not implemented.");
                        }

                        string[] array3 = text2.Split(commaSeparator);
                        string[] array4 = array3;
                        foreach (string s in array4)
                        {
                            StoreExpressionVals(0, s, num);
                        }

                        num++;
                    }
                }

                if (num <= 5)
                {
                    throw new FormatException("Unexpected end of expression.");
                }

                if (num <= 6)
                {
                    StoreExpressionVals(0, "*", 6);
                }

                ISet<int> set = GetSet(5);
                ISet<int> set2 = GetSet(3);
                bool flag = !set2.Contains(98);
                bool flag2 = !set.Contains(98);
                if ((!flag || flag2) && (!flag2 || flag))
                {
                    throw new FormatException("Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.");
                }
            }
            catch (FormatException)
            {
                throw;
            }
            catch (Exception ex2)
            {
                throw new FormatException("Illegal cron expression format (" + ex2.Message + ")", ex2);
            }
        }

        private ISet<int> GetSet(int type)
        {
            return type switch
            {
                0 => seconds,
                1 => minutes,
                2 => hours,
                3 => daysOfMonth,
                4 => months,
                5 => daysOfWeek,
                6 => years,
                _ => throw new ArgumentOutOfRangeException(),
            };
        }

        private int StoreExpressionVals(int pos, string s, int type)
        {
            int incr = 0;
            int num = SkipWhiteSpace(pos, s);
            if (num >= s.Length)
            {
                return num;
            }

            char c = s[num];
            if (c >= 'A' && c <= 'Z' && !s.Equals("L") && !s.Equals("LW") && !regex.IsMatch(s))
            {
                string text = s.Substring(num, 3);
                int num2 = -1;
                int num3;
                switch (type)
                {
                    case 4:
                        num3 = GetMonthNumber(text) + 1;
                        if (num3 <= 0)
                        {
                            throw new FormatException("Invalid Month value: '" + text + "'");
                        }

                        if (s.Length <= num + 3)
                        {
                            break;
                        }

                        c = s[num + 3];
                        if (c == '-')
                        {
                            num += 4;
                            text = s.Substring(num, 3);
                            num2 = GetMonthNumber(text) + 1;
                            if (num2 <= 0)
                            {
                                throw new FormatException("Invalid Month value: '" + text + "'");
                            }
                        }

                        break;
                    case 5:
                        num3 = GetDayOfWeekNumber(text);
                        if (num3 < 0)
                        {
                            throw new FormatException("Invalid Day-of-Week value: '" + text + "'");
                        }

                        if (s.Length <= num + 3)
                        {
                            break;
                        }

                        switch (s[num + 3])
                        {
                            case '-':
                                num += 4;
                                text = s.Substring(num, 3);
                                num2 = GetDayOfWeekNumber(text);
                                if (num2 < 0)
                                {
                                    throw new FormatException("Invalid Day-of-Week value: '" + text + "'");
                                }

                                break;
                            case '#':
                                try
                                {
                                    num += 4;
                                    nthdayOfWeek = Convert.ToInt32(s.Substring(num), CultureInfo.InvariantCulture);
                                    if (nthdayOfWeek < 1 || nthdayOfWeek > 5)
                                    {
                                        throw new Exception();
                                    }
                                }
                                catch (Exception)
                                {
                                    throw new FormatException("A numeric value between 1 and 5 must follow the '#' option");
                                }

                                break;
                            case '/':
                                try
                                {
                                    num += 4;
                                    everyNthWeek = Convert.ToInt32(s.Substring(num), CultureInfo.InvariantCulture);
                                    if (everyNthWeek < 1 || everyNthWeek > 5)
                                    {
                                        throw new Exception();
                                    }
                                }
                                catch (Exception)
                                {
                                    throw new FormatException("A numeric value between 1 and 5 must follow the '/' option");
                                }

                                break;
                            case 'L':
                                lastdayOfWeek = true;
                                num++;
                                break;
                            default:
                                throw new FormatException("Illegal characters for this position: '" + text + "'");
                        }

                        break;
                    default:
                        throw new FormatException("Illegal characters for this position: '" + text + "'");
                }

                if (num2 != -1)
                {
                    incr = 1;
                }

                AddToSet(num3, num2, incr, type);
                return num + 3;
            }

            if (c == '?')
            {
                num++;
                if (num + 1 < s.Length && s[num] != ' ' && s[num + 1] != '\t')
                {
                    throw new FormatException("Illegal character after '?': " + s[num]);
                }

                if (type != 5 && type != 3)
                {
                    throw new FormatException("'?' can only be specified for Day-of-Month or Day-of-Week.");
                }

                if (type == 5 && !lastdayOfMonth)
                {
                    int num4 = daysOfMonth.LastOrDefault();
                    if (num4 == 98)
                    {
                        throw new FormatException("'?' can only be specified for Day-of-Month -OR- Day-of-Week.");
                    }
                }

                AddToSet(98, -1, 0, type);
                return num;
            }

            bool flag = c == '*';
            if (!flag)
            {
                switch (c)
                {
                    case '/':
                        break;
                    case 'L':
                        num++;
                        if (type == 3)
                        {
                            lastdayOfMonth = true;
                        }

                        if (type == 5)
                        {
                            AddToSet(7, 7, 0, type);
                        }

                        if (type == 3 && s.Length > num)
                        {
                            c = s[num];
                            if (c == '-')
                            {
                                ValueSet value2 = GetValue(0, s, num + 1);
                                lastdayOffset = value2.theValue;
                                if (lastdayOffset > 30)
                                {
                                    throw new FormatException("Offset from last day must be <= 30");
                                }

                                num = value2.pos;
                            }

                            if (s.Length > num)
                            {
                                c = s[num];
                                if (c == 'W')
                                {
                                    nearestWeekday = true;
                                    num++;
                                }
                            }
                        }

                        return num;
                    case '0':
                    case '1':
                    case '2':
                    case '3':
                    case '4':
                    case '5':
                    case '6':
                    case '7':
                    case '8':
                    case '9':
                        {
                            int num5 = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture);
                            num++;
                            if (num >= s.Length)
                            {
                                AddToSet(num5, -1, -1, type);
                                return num;
                            }

                            c = s[num];
                            if (c >= '0' && c <= '9')
                            {
                                ValueSet value = GetValue(num5, s, num);
                                num5 = value.theValue;
                                num = value.pos;
                            }

                            return CheckNext(num, s, num5, type);
                        }
                    default:
                        throw new FormatException($"Unexpected character: {c}");
                }
            }

            if (flag && num + 1 >= s.Length)
            {
                AddToSet(99, -1, incr, type);
                return num + 1;
            }

            if (c == '/' && (num + 1 >= s.Length || s[num + 1] == ' ' || s[num + 1] == '\t'))
            {
                throw new FormatException("'/' must be followed by an integer.");
            }

            if (flag)
            {
                num++;
            }

            c = s[num];
            if (c == '/')
            {
                num++;
                if (num >= s.Length)
                {
                    throw new FormatException("Unexpected end of string.");
                }

                incr = GetNumericValue(s, num);
                num++;
                if (incr > 10)
                {
                    num++;
                }

                CheckIncrementRange(incr, type);
            }
            else
            {
                if (flag)
                {
                    throw new FormatException("Illegal characters after asterisk: " + s);
                }

                incr = 1;
            }

            AddToSet(99, -1, incr, type);
            return num;
        }
        private int CheckNext(int pos, string s, int val, int type)
        {
            int end = -1;
            int num = pos;
            if (num >= s.Length)
            {
                AddToSet(val, end, -1, type);
                return num;
            }

            switch (s[pos])
            {
                case 'L':
                    if (type == 5)
                    {
                        if (val < 1 || val > 7)
                        {
                            throw new FormatException("Day-of-Week values must be between 1 and 7");
                        }

                        lastdayOfWeek = true;
                        ISet<int> set2 = GetSet(type);
                        set2.Add(val);
                        return num + 1;
                    }

                    throw new FormatException($"'L' option is not valid here. (pos={num})");
                case 'W':
                    if (type == 3)
                    {
                        nearestWeekday = true;
                        if (val > 31)
                        {
                            throw new FormatException("The 'W' option does not make sense with values larger than 31 (max number of days in a month)");
                        }

                        ISet<int> set4 = GetSet(type);
                        set4.Add(val);
                        return num + 1;
                    }

                    throw new FormatException($"'W' option is not valid here. (pos={num})");
                case '#':
                    {
                        if (type != 5)
                        {
                            throw new FormatException($"'#' option is not valid here. (pos={num})");
                        }

                        num++;
                        try
                        {
                            nthdayOfWeek = Convert.ToInt32(s.Substring(num), CultureInfo.InvariantCulture);
                            if (nthdayOfWeek < 1 || nthdayOfWeek > 5)
                            {
                                throw new Exception();
                            }
                        }
                        catch (Exception)
                        {
                            throw new FormatException("A numeric value between 1 and 5 must follow the '#' option");
                        }

                        ISet<int> set3 = GetSet(type);
                        set3.Add(val);
                        return num + 1;
                    }
                case 'C':
                    {
                        switch (type)
                        {
                            case 5:
                                calendardayOfWeek = true;
                                break;
                            case 3:
                                calendardayOfMonth = true;
                                break;
                            default:
                                throw new FormatException($"'C' option is not valid here. (pos={num})");
                        }

                        ISet<int> set = GetSet(type);
                        set.Add(val);
                        return num + 1;
                    }
                case '-':
                    {
                        num++;
                        int num3 = Convert.ToInt32(s[num].ToString(), CultureInfo.InvariantCulture);
                        end = num3;
                        num++;
                        if (num >= s.Length)
                        {
                            AddToSet(val, end, 1, type);
                            return num;
                        }

                        char c = s[num];
                        if (c >= '0' && c <= '9')
                        {
                            ValueSet value2 = GetValue(num3, s, num);
                            int theValue2 = value2.theValue;
                            end = theValue2;
                            num = value2.pos;
                        }

                        if (num < s.Length && s[num] == '/')
                        {
                            num++;
                            int num4 = Convert.ToInt32(s[num].ToString(), CultureInfo.InvariantCulture);
                            num++;
                            if (num >= s.Length)
                            {
                                AddToSet(val, end, num4, type);
                                return num;
                            }

                            c = s[num];
                            if (c >= '0' && c <= '9')
                            {
                                ValueSet value3 = GetValue(num4, s, num);
                                int theValue3 = value3.theValue;
                                AddToSet(val, end, theValue3, type);
                                return value3.pos;
                            }

                            AddToSet(val, end, num4, type);
                            return num;
                        }

                        AddToSet(val, end, 1, type);
                        return num;
                    }
                case '/':
                    {
                        if (num + 1 >= s.Length || s[num + 1] == ' ' || s[num + 1] == '\t')
                        {
                            throw new FormatException("'/' must be followed by an integer.");
                        }

                        num++;
                        int num2 = Convert.ToInt32(s[num].ToString(), CultureInfo.InvariantCulture);
                        num++;
                        if (num >= s.Length)
                        {
                            CheckIncrementRange(num2, type);
                            AddToSet(val, end, num2, type);
                            return num;
                        }

                        char c = s[num];
                        if (c >= '0' && c <= '9')
                        {
                            ValueSet value = GetValue(num2, s, num);
                            int theValue = value.theValue;
                            CheckIncrementRange(theValue, type);
                            AddToSet(val, end, theValue, type);
                            return value.pos;
                        }

                        throw new FormatException($"Unexpected character '{c}' after '/'");
                    }
                default:
                    AddToSet(val, end, 0, type);
                    return num + 1;
            }
        }
        private ValueSet GetValue(int v, string s, int i)
        {
            char c = s[i];
            StringBuilder stringBuilder = new StringBuilder(v.ToString(CultureInfo.InvariantCulture));
            while (c >= '0' && c <= '9')
            {
                stringBuilder.Append(c);
                i++;
                if (i >= s.Length)
                {
                    break;
                }

                c = s[i];
            }

            ValueSet valueSet = new ValueSet();
            if (i < s.Length)
            {
                valueSet.pos = i;
            }
            else
            {
                valueSet.pos = i + 1;
            }

            valueSet.theValue = Convert.ToInt32(stringBuilder.ToString(), CultureInfo.InvariantCulture);
            return valueSet;
        }

        private int GetNumericValue(string s, int i)
        {
            int num = FindNextWhiteSpace(i, s);
            string value = s.Substring(i, num - i);
            return Convert.ToInt32(value, CultureInfo.InvariantCulture);
        }
        private int FindNextWhiteSpace(int i, string s)
        {
            while (i < s.Length && (s[i] != ' ' || s[i] != '\t'))
            {
                i++;
            }
            return i;
        }
        private static void CheckIncrementRange(int incr, int type)
        {
            if (incr > 59 && (type == 0 || type == 1))
            {
                throw new FormatException($"Increment > 60 : {incr}");
            }

            if (incr > 23 && type == 2)
            {
                throw new FormatException($"Increment > 24 : {incr}");
            }

            if (incr > 31 && type == 3)
            {
                throw new FormatException($"Increment > 31 : {incr}");
            }

            if (incr > 7 && type == 5)
            {
                throw new FormatException($"Increment > 7 : {incr}");
            }

            if (incr > 12 && type == 4)
            {
                throw new FormatException($"Increment > 12 : {incr}");
            }
        }
        private int GetDayOfWeekNumber(string s)
        {
            if (dayMap.ContainsKey(s))
            {
                return dayMap[s];
            }

            return -1;
        }
        private void AddToSet(int val, int end, int incr, int type)
        {
            ISet<int> set = GetSet(type);
            switch (type)
            {
                case 0:
                case 1:
                    if ((val < 0 || val > 59 || end > 59) && val != 99)
                    {
                        throw new FormatException("Minute and Second values must be between 0 and 59");
                    }
                    break;
                case 2:
                    if ((val < 0 || val > 23 || end > 23) && val != 99)
                    {
                        throw new FormatException("Hour values must be between 0 and 23");
                    }

                    break;
                case 3:
                    if ((val < 1 || val > 31 || end > 31) && val != 99 && val != 98)
                    {
                        throw new FormatException("Day of month values must be between 1 and 31");
                    }

                    break;
                case 4:
                    if ((val < 1 || val > 12 || end > 12) && val != 99)
                    {
                        throw new FormatException("Month values must be between 1 and 12");
                    }

                    break;
                case 5:
                    if ((val == 0 || val > 7 || end > 7) && val != 99 && val != 98)
                    {
                        throw new FormatException("Day-of-Week values must be between 1 and 7");
                    }

                    break;
            }

            if (incr == 0 || incr == -1)
            {
                switch (val)
                {
                    default:
                        set.Add(val);
                        return;
                    case -1:
                        set.Add(98);
                        return;
                    case 99:
                        break;
                }
            }

            int num = val;
            int num2 = end;
            if (val == 99 && incr <= 0)
            {
                incr = 1;
                set.Add(99);
            }

            switch (type)
            {
                case 0:
                case 1:
                    if (num2 == -1)
                    {
                        num2 = 59;
                    }

                    if (num == -1 || num == 99)
                    {
                        num = 0;
                    }

                    break;
                case 2:
                    if (num2 == -1)
                    {
                        num2 = 23;
                    }

                    if (num == -1 || num == 99)
                    {
                        num = 0;
                    }

                    break;
                case 3:
                    if (num2 == -1)
                    {
                        num2 = 31;
                    }

                    if (num == -1 || num == 99)
                    {
                        num = 1;
                    }

                    break;
                case 4:
                    if (num2 == -1)
                    {
                        num2 = 12;
                    }

                    if (num == -1 || num == 99)
                    {
                        num = 1;
                    }

                    break;
                case 5:
                    if (num2 == -1)
                    {
                        num2 = 7;
                    }

                    if (num == -1 || num == 99)
                    {
                        num = 1;
                    }

                    break;
                case 6:
                    if (num2 == -1)
                    {
                        num2 = MaxYear;
                    }

                    if (num == -1 || num == 99)
                    {
                        num = 1970;
                    }

                    break;
            }

            int num3 = -1;
            if (num2 < num)
            {
                num3 = type switch
                {
                    0 => 60,
                    1 => 60,
                    2 => 24,
                    4 => 12,
                    5 => 7,
                    3 => 31,
                    6 => throw new ArgumentException("Start year must be less than stop year"),
                    _ => throw new ArgumentException("Unexpected type encountered"),
                };
                num2 += num3;
            }

            for (int i = num; i <= num2; i += incr)
            {
                if (num3 == -1)
                {
                    set.Add(i);
                    continue;
                }

                int num4 = i % num3;
                if (num4 == 0 && (type == 4 || type == 5 || type == 3))
                {
                    num4 = num3;
                }

                set.Add(num4);
            }
        }

        private int GetMonthNumber(string s)
        {
            if (monthMap.ContainsKey(s))
            {
                return monthMap[s];
            }

            return -1;
        }
        private int SkipWhiteSpace(int i, string s)
        {
            while (i < s.Length && (s[i] == ' ' || s[i] == '\t'))
            {
                i++;
            }

            return i;
        }

        public override string ToString()
        {
            return cronExpression;
        }
    }

    class ValueSet
    {
        //
        // 摘要:
        //     The value.
        public int theValue;

        //
        // 摘要:
        //     The position.
        public int pos;
    }
    public static class CronE
    {
        public static bool TryContainsOrAfter(this SortedSet<int> sortset, int item)
        {
            if (sortset.Contains(item))
            {
                return true;
            }
            return TryContainsOrAfter(sortset, item, out _);
        }
        public static bool TryContainsOrAfter(this SortedSet<int> sortset, int item, out int result)
        {
            int i = 0;
            foreach (var t in sortset)
            {
                if (t >= item)
                {
                    result = t;
                    return true;
                }
                i++;
            }
            result = 0;
            return false;
        }
        public static SortedSet<int> TailSet(this SortedSet<int> sortset, int item)
        {
            int i = 0;
            foreach (var t in sortset)
            {
                if (t >= item)
                {
                    return new SortedSet<int>(sortset.Skip(i));
                }
                i++;
            }
            return new SortedSet<int>();
        }
    }
}
